<?php
/*
 * Quark PHP Framework
 * Copyright 2011, Sahib Alejandro Jaramillo Leo.
 * http://quarkphp.com/
 * Licensed under the GNU General Public License (http://www.gnu.org/licenses/gpl.html).
 */
/**
 * Definicion de clase base DbObject
 * @filesource
 */
/**
 * Clase base DbObject
 * @author sahib
 */
class DbObject extends PDO {

	/**
	 * Listado de campos para seleccionar en una consulta SELECT
	 * @access private 
	 * @var string
	 */
	private $fields;

	/**
	 * Inicio del LIMIT
	 * @access private
	 * @var int
	 */
	private $limit_start;

	/**
	 * Tamaño del LIMIT
	 * @access private
	 * @var int
	 */
	private $limit_length;

	/**
	 * WHERE para aplicar a la consulta
	 * @access private
	 * @var string
	 */
	private $where;

	/**
	 * Campos para realizar el ORDER BY en una consulta
	 * @access private
	 * @var unknown_type
	 */
	private $orderby;

	/**
	 * Array de parametros para utilizar en los place holders de una consulta
	 * @access private
	 * @var array
	 */
	private $params;

	/**
	 * Flag para definir si se reincian o no las propiedades de consulta despues de invocar algunos metodos como delete, update, _select, etc.
	 * Por default TRUE
	 * @var bool
	 */
	private $reset_query_properties;

	/**
	 * Lista de JOINS
	 * @var array
	 */
	private $joins;

	/**
	 * Constructor
	 * @access public
	 * @see database.php
	 * @return DbObject
	 */
	public function __construct( ) {

		// Cargar configuracion BASE para la base de datos.
		$database = array();
		require 'quark/base_database.php';
		$base_db_config = $database;
		$database = NULL;
		
		// Cargar configuracion del usuario y sobre escribir en BASE
		require 'config/database.php';
		foreach( $base_db_config as $key => $val )
			if( !isset( $database[$key] ) )
				$database[$key] = $val;
		unset( $base_db_config );
		
		/*
		 * TODO
		 * Implementar más DSN para mos drivers
		 */
		$dsn_drivers = array( 
			'mysql' => "mysql:host={$database['host']};dbname={$database['database']}" 
		);
		
		parent::__construct( $dsn_drivers[$database['driver']], $database['user'], $database['passwd'], $database['options'] );
		
		// Configurar PDO
		$this->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
		$this->setAttribute( PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ );
		
		/*
		 * TODO
		 * Establecer la codificacion de la coneccion
		 * Actualmente solo esta dando soporte a MySQL
		 * Hay que implementar el soporte para mos
		 * bases de datos.
		 */
		if( $database['driver'] == 'mysql' ) {
			$St = $this->prepare( 'SET NAMES ?' );
			$St->bindValue( 1, $database['charset'], PDO::PARAM_STR );
		}
		$St->execute( );
		
		// Inicializar las "query properties"
		$this->reset_query_properties = TRUE;
		$this->resetQueryProperties( );
	}

	/**
	 * Ejecuta un SELECT sobre la tabla $table
	 * @access public
	 * @param string $table Nombre de la tabla
	 * @return array Array de registros
	 */
	public function select( $table ) {

		return $this->_select( $table )
			->fetchAll( );
	}

	/**
	 * Ejecuta un SELECT LIMIT 0,1 sobre la tabla $table
	 * @access public
	 * @param string $table Nombre de la tabla
	 * @return stdClass Registro seleccionado
	 */
	public function selectOne( $table ) {

		// Respaldar limit
		$ls = $this->limit_start;
		$ll = $this->limit_length;
		
		$Row = $this->setLimit( 0, 1 )
			->_select( $table )
			->fetch( );
		
		// Restaurar limit
		$this->setLimit( $ls, $ll );
		
		return $Row;
	}

	/**
	 * Equivalente a hacer un SELECT COUNT(*) sobre la tabla $table, los valores de setFields y setLimit son ingnorados.
	 * NOTA: NO reinicia las query properties.
	 * 
	 * @access public
	 * @param string $table Nombre de la tabla
	 * @return int Numero de filas
	 */
	public function selectCount( $table ) {

		// Respaldar propiedades
		$fields = $this->fields;
		$limit_start = $this->limit_start;
		$limit_length = $this->limit_start;
		
		// Sobre escribir propiedades
		$this->limit_start = NULL;
		$this->limit_length = NULL;
		$this->setFields( 'COUNT(*)' );
		
		$reset_properties = $this->setResetQueryProperties( FALSE );
		
		$cnt = (int)$this->_select( $table )
			->fetchColumn( 0 );
		
		$this->setResetQueryProperties( $reset_properties );
		
		// Restaurar propiedades
		$this->setFields( $fields );
		$this->setLimit( $limit_start, $limit_length );
		
		return $cnt;
	}

	/**
	 * Inserta un nuevo registro en la tabla $table con los datos $fields
	 * @access public
	 * @param string $table Nombre de la tabla
	 * @param array $fields array asociativo de campos a insertar en formato 'campo' => valor
	 * @return int ID del registro insertado
	 */
	public function insert( $table, $fields ) {

		$fields_names_list = $place_holders_list = array();
		
		foreach( $fields as $field_name => $value ) {
			$value = $this->fixValue( $value );
			$fields_names_list[] = $field_name;
			/*
			 * Formatear el string del placeholder, que puede ser:
			 * "?", "FUNCTION(?)" o "FUNCTION(custom,arguments,?)"
			 */
			$place_holders_list[] = $this->createPlaceHolder( $value );
			
			// Agregar el valor a la lista de parametros.
			$this->addParam( is_array( $value ) ? $value['value'] : $value, is_array( $value ) ? $value['type'] : NULL );
		}
		
		$sql = 'INSERT INTO ' . $table . '(' . implode( ',', $fields_names_list ) . ')VALUES(' . implode( ',', $place_holders_list ) . ')';
		$St = $this->prepare( $sql );
		$this->bindParams( $St );
		$St->execute( );
		
		$this->resetQueryProperties( );
		
		return $this->lastInsertId( );
	}

	/**
	 * Ejecuta un UPDATE sobre $tabla, actualizando los campos $fields
	 * @access public
	 * @param string $table Nombre de la tabla
	 * @param array $fields array asociativo con formato 'campo1' => valor1, 'campoN' => valorN
	 * @return int Numero de filas afectadas
	 */
	public function update( $table, $fields ) {

		/*
		 * Necesitamos invertir el orden de $fields
		 * ya que en update es muy probable que exista un "WHERE"
		 * por lo cual, los params de $fields deberon estar ANTES
		 * de los parames agregados antes la llamada.
		 * 
		 * --------------------------------------------------
		 * Considera el siguiente ejemplo:
		 * 
		 * $this->setWhere('id=?')->addParam(10)->update('products', array('name'=>'New Product'));
		 * 
		 * El ID del producto estaro primero en la lista de parametros, pero en realidad
		 * pertenece al WHERE, que por lo general debero estar al final.
		 * 
		 * Cualquier consulta que no sea compatible con este orden de parametros debero
		 * ser programada por el usuario en su propio model.
		 */
		$fields_list = array();
		$fields = array_reverse( $fields );
		
		foreach( $fields as $field_name => $value ) {
			$value = $this->fixValue( $value );
			$fields_list[] = $field_name . '=' . $this->createPlaceHolder( $value );
			
			// En este caso, no utilizamos "addParam", por que agregamos los params al inicio.
			array_unshift( $this->params, 
				array( 
					'value' => (is_array( $value ) ? $value['value'] : $value), 
					'type' => (is_array( $value ) ? $value['type'] : PDO::PARAM_STR) 
				) );
		}
		
		$sql = 'UPDATE ' . $table . ' SET ' . implode( ',', array_reverse( $fields_list ) ) . $this->getWhere( ) . $this->getLimit( );
		$St = $this->prepare( $sql );
		$this->bindParams( $St );
		$St->execute( );
		
		$this->resetQueryProperties( );
		
		return $St->rowCount( );
	}

	/**
	 * Ejecuta un DELETE sobre la tabla $table
	 * @access public
	 * @param string $table nombre de la tabla
	 * @return int numero de filas afectadas
	 */
	public function delete( $table ) {

		$sql = 'DELETE FROM ' . $table . $this->getWhere( ) . $this->getLimit( );
		$St = $this->prepare( $sql );
		$this->bindParams( $St );
		$St->execute( );
		
		$this->resetQueryProperties( );
		
		return $St->rowCount( );
	}

	/**
	 * Agrega parametros para los place holders de la consulta.
	 * @access public
	 * @param mixed $param valor del parametro
	 * @param int $type tipo de dato, por default PDO::PARAM_STR
	 */
	public function addParam( $param, $type = NULL ) {

		// type automatico si $type es NULL
		if( $type == NULL )
			$type = (is_numeric( $param ) ? PDO::PARAM_INT : PDO::PARAM_STR);
		
		$this->params[] = array( 
			'value' => $param, 
			'type' => $type 
		);
		return $this;
	}

	/**
	 * Bindea todos los parametros agregados con "$this->addParam(...)" al PDOStatement $St
	 * @access private
	 * @param PDOStatement $St PDOStatement donde bindear los parametros
	 */
	private function bindParams( PDOStatement $St ) {

		if( count( $this->params ) > 0 ) {
			for( $i = 0; $i < count( $this->params ); $i++ )
				$St->bindValue( $i + 1, $this->params[$i]['value'], $this->params[$i]['type'] );
		}
	}

	/**
	 * Ejecuta la consulta SELECT sobre tabla $table
	 * @access private
	 * @param string $table nombre de la tabla
	 * @return PDOStatement PDOStatement de la consulta
	 */
	private function _select( $table ) {

		// Generar consulta SQL
		$sql = 'SELECT ' . $this->fields . ' FROM ' . $table . $this->getJoins( ) . $this->getWhere( ) . $this->getOrderBy( ) .
			 $this->getLimit( );
			
			 Quark::log($sql);
			 
		// Preparar y ejecutar consulta
		$St = $this->prepare( $sql );
		$this->bindParams( $St );
		$St->execute( );
		
		$this->resetQueryProperties( );
		
		// Devolver registro.
		return $St;
	}

	/**
	 * Devuelve un string valido para usar como place holder
	 * Si $v es un array, se asume que es la descripcion de un place holder: value,function,function_arguments,etc.
	 * @access private
	 * @param mixed $v valor del place holder
	 */
	private function createPlaceHolder( $v ) {

		return ((is_array( $v ) && !empty( $v['function'] ) ? $v['function'] .
			 (empty( $v['function_arguments'] ) ? '(?)' : '(' . $v['function_arguments'] . ')') : '?'));
	}

	/**
	 * Fixea un argumento que es utilizado para por update o insert para especificar correctamente tipo y funcion en caso de que el argumento sea un array.
	 * @access private
	 * @param mixed $value valor a fixear
	 * @return mixed valor fixeado
	 */
	private function fixValue( $value ) {

		if( is_array( $value ) ) {
			if( !isset( $value['type'] ) )
				$value['type'] = NULL;
			if( !isset( $value['function'] ) )
				$value['function'] = NULL;
			if( !isset( $value['function_arguments'] ) )
				$value['function_arguments'] = NULL;
		}
		return $value;
	}

	/**
	 * Establece los campos que se utilizaron en la consulta select
	 * @access public
	 * @param string $fields lista de campos separados por comas "campo1, campo2, campoN"
	 * @return DbObject $this para hacer linking
	 */
	public function setFields( $fields ) {

		$this->fields = $fields;
		return $this;
	}

	/**
	 * Establece el valor para LIMIT de la consulta
	 * @access public
	 * @param int $start inicio del limit
	 * @param int $length tamaño del limit
	 * @return DbObject $this para hacer linking
	 */
	public function setLimit( $start, $length ) {

		$this->limit_start = $start !== NULL ? (int)$start : NULL;
		$this->limit_length = $length !== NULL ? (int)$length : NULL;
		return $this;
	}

	/**
	 * Establece la clausula WHERE para la consulta
	 * @access public
	 * @param string $where condicion where (sin prefijo "WHERE")
	 * @return DbObject $this para hacer linking
	 */
	public function setWhere( $where ) {

		$this->where = $where;
		return $this;
	}

	/**
	 * Establece la clausula ORDER BY para la consulta
	 * @access public
	 * @param string $orderby order by (sin prefijo "ORDER BY")
	 * @return DbObject $this para hacer linking
	 */
	public function setOrderBy( $orderby ) {

		$this->orderby = $orderby;
		return $this;
	}

	/**
	 * Devuelve la instruccion completa LIMIT
	 * @access private
	 * @return string instruccion completa LIMIT
	 */
	private function getLimit( ) {

		return ($this->limit_start !== NULL ? ' LIMIT ' . $this->limit_start . ',' . $this->limit_length . ' ' : '');
	}

	/**
	 * Devuelve la instruccion completa ORDER BY
	 * @access private
	 * @return string instruccion completa ORDER BY
	 */
	private function getOrderBy( ) {

		return ($this->orderby != NULL ? ' ORDER BY ' . $this->orderby . ' ' : '');
	}

	/**
	 * Devuelve la instruccion completa WHERE
	 * @access private
	 * @return string instruccion completa WHERE
	 */
	private function getWhere( ) {

		return ($this->where != NULL ? ' WHERE ' . $this->where . ' ' : '');
	}

	/**
	 * Establece el flag reset_query_properties, devuelve el antiguo valor.
	 * @param bool $reset
	 * @return bool
	 */
	protected function setResetQueryProperties( $reset ) {

		$old = $this->reset_query_properties;
		$this->reset_query_properties = $reset;
		return $old;
	}

	/**
	 * Devuelve el valor actual de reset_query_properties
	 * @return bool
	 */
	protected function getResetQueryProperties( ) {

		return $this->reset_query_properties;
	}

	/**
	 * Reinicia los datos establecidos por los metodos set*
	 * 
	 * @access public
	 * @return DbObject $this para hacer linking
	 */
	public function resetQueryProperties( ) {

		if( $this->reset_query_properties ) {
			$this->fields = '*';
			$this->limit_start = NULL;
			$this->limit_length = NULL;
			$this->where = NULL;
			$this->orderby = NULL;
			$this->params = array();
			$this->joins = array();
		}
		return $this;
	}

	/**
	 * Agrega un join a la lista de joins
	 * 
	 * @param string $type tipo de JOIN, puede ser LEFT, RIGHT, etc.
	 * @param string $table nombre de la tabla para el JOIN
	 * @param string $cond condicion para el JOIN
	 */
	public function setJoin( $type, $table, $cond ) {

		$Join = new stdClass( );
		
		$Join->type = $type;
		$Join->table = $table;
		$Join->cond = $cond;
		
		$this->joins[] = $Join;
		
		return $this;
	}

	/**
	 * Devuelve la cadena de joins
	 * @return string
	 */
	private function getJoins( ) {

		$joins = ' ';
		foreach( $this->joins as $Join )
			$joins .= $Join->type . ' JOIN ' . $Join->table . ' ON (' . $Join->cond . ') ';
		return $joins;
	}
	
	/**
	 * Limpia un valor,
	 * si es numerico devuelve su valor númerico real,
	 * si es una cadena devuelve la cadena despues de aplicarle trim()
	 * 
	 * @param mixed $val
	 * @return mixed Valor limpio.
	 */
	public static function cleanValue($val){
		$val=trim($val);
		if( is_numeric($val) and strpos($val, '.') !== FALSE ){
			return floatval($val);
		}
		elseif( is_numeric($val) ){
			return intval($val);
		}
		else{
			return $val;
		}
	}
}